psql利用時にサーバ証明書の検証を強制してみた
こんにちは。中村です。
はじめに
RDSでpostgreSQLを利用する場合、エンジンバージョンが15.x以上になると、SSL/TLS接続がデフォルト設定で強制となります。
普段何気なくpsqlを利用してRDSへ接続しているけれど、証明書の検証ってどうなっているの?と疑問に思うことがあり、検証してみました。
結論
- psqlではデフォルトで証明書の検証を行なっていませんでした
- psqlでサーバ証明書の検証を行うためには、「PGSSLMODE=verify-full」など明示的に指定する必要があります
調べてみた
psqlはデフォルトでサーバ証明書を検証していない
使用されるデフォルトの sslmode モードは、libpq ベースのクライアント (psql など) と JDBC では異なります。libpq ベースのクライアントはデフォルトで prefer に設定されますが、JDBC クライアントはデフォルトで verify-full に設定されます。
AWS公式ドキュメントにおいて、sslmodeが、利用するクライアントソフトによって異なる旨、説明されていました。
デフォルトではPostgreSQLはサーバ証明書の検証をまったく行いません。 これは、(例えば、DNSレコードを変更したり、もしくはサーバのIPアドレスを乗っ取ったりして)クライアントに知られずにサーバの身元をなりすませることを意味します。
postgreSQLのドキュメントにおいても、デフォルトでサーバ証明書の検証が実施されていない旨、案内がされています。
普段何気なくpsqlを利用していて、「SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)」などSSL通信できているようなレスポンスがあっても、実はデフォルト設定だと、サーバ証明書の検証をしていなかったのですね。
話の脱線
そもそもなぜ、
私がpsqlで証明書の検証がされているかどうか自体に気になり始めたのかというと、
RDSのルート証明書ってどこに保存されているのだろうか?
ということに疑問に感じたからでした。
そのとき、「/etc/pki/」配下で管理しているのだろうなと思ったものの、ただ、証明書の中身を確認するのは大変かなと思いました。
そこで、ディレクトリ自体をリネーム(もしくは、明らかに異なるルート証明書1つだけを設定)してエラーとなることを確認してみよう!という方針でひとまずpkiディレクトリ名を変更してみました。
結果、相変わらず、SSL/TLS通信ができてしまった。
ここで、前提が間違っているかもしれないと、psqlのSSL通信の仕組みを振り返ることにしたという流れでした。
結論は記載の通りです。
ちなみに、AWS CloudShellで検証していました。
AWS公式ドキュメントで明示的な案内は、見つけられませんでしたが、
「/certs/rds-global.pem」にもRDS用のCA証明書が保存されていそうです。
AWS公式からは、証明書バンドルをダウンロードする方法が案内されています。
データベースに接続するときに使用する証明書バンドルをダウンロードします。証明書バンドルをダウンロードするには、 AWS リージョン による証明書バンドル. を参照してください。
やってみる
ということで、psqlを利用する際は、デフォルトにてサーバ証明書の検証がされていないことが判明したので、明示的にsslmodeの設定した通信を試してみます。
前提条件
- psql (PostgreSQL) 15.8
- PostgreSQL 15.8
やってみた
準備
本記事の実行環境として、RDSをパブリックサブネットに配置し、AWS CloudShellからpsql接続を試します。
RDSをプライベートサブネットに配置したい場合は、RDSを配置したVPC内部にEC2などのpsqlを実行できる環境を別途ご用意ください。
※通常、DBサーバはプライベートサブネットに配置することを推奨します。
まず、CloudShellのグローバルIPアドレスを下記コマンドを実行して確認しておきます。
curl http://checkip.amazonaws.com/
次に、RDSサーバを構築します。
下記CloudFormationテンプレートを利用する場合は、MyIPにCloudShellのグローバルIPアドレス(xxx.xxx.xxx.xxx/32)を記載してください。
合わせて、RDSMasterUserPasswordも入力ください。
CloudFormationテンプレート参考
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
AWS CloudFormation Template:
This template make temp RDS.
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: Common Configuration
Parameters:
- AppName
- MyIP
- RDSMasterUserPassword
Parameters:
AppName:
Description: Name of this application.
Type: String
Default: rds-playground
MyIP:
Description: IP address allowed to connect
Type: String
RDSMasterUserPassword:
Type: String
NoEcho: true
Description: The database admin account password
Mappings:
SubnetConfig:
VPC:
CIDR: 10.20.0.0/16
PublicSubnet1:
CIDR: 10.20.11.0/24
PublicSubnet2:
CIDR: 10.20.12.0/24
Resources:
# ==========
# VPC
# ==========
VPC:
Type: AWS::EC2::VPC
Properties:
EnableDnsSupport: true
EnableDnsHostnames: true
CidrBlock: !FindInMap [SubnetConfig, VPC, CIDR]
Tags:
- Key: Name
Value: !Sub ${AppName}-VPC
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${AppName}-VPC-IGW
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId:
!Ref VPC
InternetGatewayId:
!Ref InternetGateway
# ==========
# Public Subnet
# ==========
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [0, !GetAZs ""]
VpcId: !Ref VPC
CidrBlock: !FindInMap [SubnetConfig, PublicSubnet1, CIDR]
Tags:
- Key: Name
Value: !Sub ${AppName}-VPC-PVTSB1
- Key: Network
Value: Public
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [1, !GetAZs ""]
VpcId: !Ref VPC
CidrBlock: !FindInMap [SubnetConfig, PublicSubnet2, CIDR]
Tags:
- Key: Name
Value: !Sub ${AppName}-VPC-PVTSB2
- Key: Network
Value: Public
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: !Sub ${AppName}-public-rtb
VpcId: !Ref VPC
Route:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
RouteTableId: !Ref PublicRouteTable
PublicSubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicSubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
# ==========
# RDS
# ==========
RDSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "Security group"
GroupName: !Sub ${AppName}-SG
SecurityGroupIngress:
- FromPort: 5432
IpProtocol: tcp
CidrIp: !Ref MyIP
ToPort: 5432
VpcId: !Ref VPC
DBInstance:
Type: AWS::RDS::DBInstance
Properties:
AllocatedStorage: 20
DBInstanceClass: db.t4g.micro
DBInstanceIdentifier: !Sub ${AppName}-rds-instance
DBName: postgres
DBParameterGroupName: !Ref DBParameterGroup
DBSubnetGroupName: !Ref DBSubnetGroup
Engine: postgres
EngineVersion: 15.8
MasterUsername: postgres
MasterUserPassword: !Ref RDSMasterUserPassword
MultiAZ: false
PubliclyAccessible: true
StorageType: gp3
VPCSecurityGroups:
- !Ref RDSSecurityGroup
DeletionPolicy: Delete
# ==========
# DB Subnet Group
# ==========
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: "Subnet group"
DBSubnetGroupName: !Sub ${AppName}-sbntgroup
SubnetIds:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
# ==========
# DB Parameter Group
# ==========
DBParameterGroup:
Type: AWS::RDS::DBParameterGroup
Properties:
DBParameterGroupName: !Sub ${AppName}-prmgrp
Description: "Parameter group"
Family: postgres15
Parameters:
timezone: Asia/Tokyo
やってみた
①デフォルト設定で接続してみる。
[cloudshell-user@ip-10-130-53-224 ~]$ psql -h rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p 5432 -U postgres -d postgres
Password for user postgres:
psql (15.8)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, compression: off)
Type "help" for help.
postgres=>
②SSLモードをverify-fullにしてみる
[cloudshell-user@ip-10-130-53-224 ~]$ PGSSLMODE=verify-full psql -h rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p 5432 -U postgres -d postgres
psql: error: connection to server at "rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com" (xxx.xxx.xxx.xxx), port 5432 failed: root certificate file "/home/cloudshell-user/.postgresql/root.crt" does not exist
Either provide the file or change sslmode to disable server certificate verification.
[cloudshell-user@ip-10-130-53-224 ~]$
psqlのデフォルトファイルパスに証明書がないためエラーとなりました。
③SSLモードをverify-fullで証明書を指定して接続する
今回構築したRDSの認証機関は「rds-ca-rsa2048-g1」でした。
下記URLを参考に、「global-bundle.pem」をダウンロードします。
続いて、ダウンロードした証明書をCloudShellにアップロードします。
[cloudshell-user@ip-10-130-53-224 ~]$ PGSSLMODE=verify-full PGSSLROOTCERT=/home/cloudshell-user/global-bundle.pem psql -h rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p 5432 -U postgres -d postgres
Password for user postgres:
psql (15.8)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, compression: off)
Type "help" for help.
postgres=>
④SSLモードをverify-fullで証明書(別リージョンの証明書)を指定して接続する
③と同様の手順にて、証明書のみ米国東部 (バージニア北部)の証明書を利用してみます。
[cloudshell-user@ip-10-130-53-224 ~]$ PGSSLMODE=verify-full PGSSLROOTCERT=/home/cloudshell-user/us-east-1-bundle.pem psql -h rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p 5432 -U postgres -d postgres
psql: error: connection to server at "rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com" (xxx.xxx.xxx.xxx), port 5432 failed: SSL error: certificate verify failed
[cloudshell-user@ip-10-130-53-224 ~]$
証明書のパスが合わないのでエラーとなりました。
さいごに
今回の実験を通して、psqlを利用する場合は、sslmodeを明示的に指定することでサーバ証明書の検証ができることがわかりました。
ただし、15.x以上ではデフォルト設定にてTLS通信が強制されるけれども、クライアントの設定によってはサーバ証明書を検証しません。
利便性とのトレードオフにはなりますが、運用ポリシーに応じてsslmodeを設定すると良いでしょう。
今回の検証が、どなたかの参考になると嬉しいです。